在接收到消息后,根据消息类型进行不同的处理。参考示例如下:
// 接收消息
const handleWsMessage = (data) => {
if (Array.isArray(data) && data.length) return;
if (data.type === "start") {
createWsMsg(data);
} else if (data.type === "stream") {
updateCurrentMessage(
{
chat_id: data.chat_id,
message: data.message,
thought: data.intermediate_steps,
},
false
);
} else if (["end", "end_cover"].includes(data.type)) {
updateCurrentMessage(
{
...data,
end: true,
thought: data.intermediate_steps || "",
messageId: data.message_id,
noAccess: false,
liked: 0,
},
data.type === "end_cover"
);
} else if (data.type === "close") {
// 本次会话结束
}
};
// type=start时
const runLogsTypes = ['tool', 'flow', 'knowledge']
createWsMsg(data) {
set((state) => {
let newChat = cloneDeep(state.messages);
let message = ''
if (runLogsTypes.includes(data.category)) {
message = JSON.parse(data.message)
} else if (data.category === 'node') {
message = data.message
}
newChat.push({
isSend: false,
message: message,
chatKey: '',
thought: data.intermediate_steps || '',
category: data.category || '',
files: [],
end: false,
user_name: '',
extra: data.extra,
type: data.category === 'node' ? 'start' : '',
knowledge_source: null,
})
return { messages: newChat }
})
}
// 更新消息
const runLogsTypes = ['tool', 'flow', 'knowledge']
updateCurrentMessage(wsdata, cover = false) {
const messages = get().messages
const isRunLog = runLogsTypes.includes(wsdata.category);
// run log类型存在嵌套情况,使用 extra 匹配 currentMessage; 否则取最近
const currentMessageIndex = isRunLog ?
messages.findLastIndex((msg) => msg.extra === wsdata.extra)
: messages.findLastIndex((msg) => !runLogsTypes.includes(msg.category))
const currentMessage = messages[currentMessageIndex]
let message = null
if (isRunLog) {
message = JSON.parse(wsdata.message)
} else if (wsdata.category === 'node') {
message = wsdata.message
} else {
message = currentMessage.message + wsdata.message
}
const newCurrentMessage = {
...currentMessage,
...wsdata,
id: isRunLog ? wsdata.extra : wsdata.messageId, // 每条消息必唯一
message: message,
thought: currentMessage.thought + (wsdata.thought ? `${wsdata.thought}\n` : ''),
files: wsdata.files || [],
category: wsdata.category || '',
source: wsdata.source,
type: wsdata.type || '',
}
messages[currentMessageIndex] = newCurrentMessage
// 会话特殊处理
if (!isRunLog) {
// start - end 之间没有内容删除load
if (newCurrentMessage.end && !(newCurrentMessage.files?.length || newCurrentMessage.thought || newCurrentMessage.message)) {
messages.pop()
}
// 删除重复消息
const prevMessage = messages[currentMessageIndex - 1];
if ((prevMessage && prevMessage.message === newCurrentMessage.message && prevMessage.thought === newCurrentMessage.thought)
|| cover) {
const removedMsg = messages.pop()
// 使用最后一条的信息作为准确信息
Object.keys(prevMessage).forEach((key) => {
prevMessage[key] = removedMsg[key]
})
}
}
set((state) => ({
messages: [...messages],
}))
}{messages.map((msg) => {
let type = "llm";
if (msg.isSend) {
type = "user";
} else if (msg.category === "divider") {
type = "separator";
} else if (msg.category === "knowledge_source" && msg.type === "end") {
type = "knowledge_source";
} else if (["tool", "flow", "knowledge"].includes(msg.category)) {
type = "runLog";
} else if (msg.thought) {
type = "system";
} else if (msg.category === 'node') {
type = "node";
}
switch (type) {
// 用户发送的消息
case "user":
return <MessageUser key={msg.id} useName={useName} data={msg} />;
// 大模型返回的消息
case "llm":
return (
<Message
key={msg.id}
data={msg}
/>
);
default:
return (
<div className="mt-2 rounded-md border p-2 text-sm" key={msg.id}>
未知消息类型
</div>
);
}
})
}示例消息数据
{
"is_bot":true,
"message":"已为你找到相关服务",
"type":"end_cover",
"category":"answer",
"intermediate_steps":null,
"files":[],
"user_id":1,
"message_id":"c48a56ae71cd460093eacf34b3f08b6d",
"source":0,
"sender":null,
"receiver":null,
"liked":0,
"extra":"\"{}\"",
"flow_id":"94062040-ffc6-4cb9-b202-39a31d958358",
"chat_id":"b792930b25c8ae5f5af6db6b1958c377",
"knowledge_source":[],
"answer_files":[],
"is_microApp":{
"id":"MicroApp-d6480",
"pageList":[
{
"key": "0",
"code": "<!DOCTYPE html>前端代码-1</html>",
"label": "首页",
"pageId": "0"
},
{
"key": "1",
"code": "<!DOCTYPE html>前端代码-2</html>",
"label": "成功页",
"pageId": "success"
}
]
}
}该消息仅在type=end_cover且category=answer时,判断is_microApp字段是否存在,如果存在则表示可以输出渲染小程序。通过监听message事件将会获取到相应的交互数据,下面以React+iframe为示例的部分主要代码片段:
useEffect(() => {
// 添加对 message 事件的监听器
window.addEventListener('message', handleIframe);
// 清除监听器
return () => {
window.removeEventListener('message', handleIframe);
};
}, []);
const handleIframe = async (event) => {
console.log(event, '会话页面---小程序接收数据 event')
if (!event.data) {
return
}
// 处理接收到的消息
let appData = event.data
if (typeof event.data === 'string') {
appData = JSON.parse(event.data);
}
const { height, type, formData } = appData
switch (type) {
case 'iframe-height': // 该类型为用于控制小程序高度
// 为保证不同屏幕的兼容性,增加2px作为冗余
iframeRef.current.style.height = height + 2 + 'px';
break;
case 'form-data': // 该类型为小程序相关按钮事件产生的数据
const newData = JSON.parse(formData);
// 根据pageId跳转到相应的页面
if (newData.appId === iframeRef.current.id) {
const page = data.is_microApp.pageList.find(item => item.pageId === newData.pageId)
if (page && iframeRef.current) {
setIframeLoading(true)
iframeRef.current.contentWindow?.document.open();
iframeRef.current.contentWindow?.document.write(page.code || '');
iframeRef.current.contentWindow?.document.close();
iframeRef.current.onload = () => {
setIframeLoading(false)
iframeRef.current.contentWindow?.postMessage(formData)
};
}
}
break;
case 'iframe-url': // 该类型用于判断是否在会话页面内嵌入iframe展示指定页面
const urlData = JSON.parse(formData);
setChatLayoutIframeUrl(urlData.url)
break
}
}
// iframe部分
<div style={{ height: iframeLoading ? 48 : '100%' }}>
<iframe
ref={iframeRef}
sandbox="allow-scripts allow-same-origin allow-modals allow-forms"
title="preview"
srcDoc=""
width="100%"
height="100%"
id={data.is_microApp.id}
className="app-iframe"
/>
</div>由于小程序的相关操作可能导致高度存在改变的情况,因此根据需要可以进行重置iframe高度的相关操作。
const resizeIframe = () => {
const innerDoc = iframeRef.current.contentDocument || iframeRef.current.contentWindow.document;
iframeRef.current.style.height = innerDoc.body.scrollHeight + 'px';
setIframeLoading(false)
}